在上一章中我们介绍了C 中的数组和字符串。在本章中,我们讨论动态分配的 C 字符串及其与 C 字符串库的使用。我们首先简要概述静态声明的字符串。
2.6.1. C 对静态分配字符串(字符数组)的支持
C 不支持单独的字符串类型,但在 C 程序中可以用'\0'
这个特别的空字符(零字节,null character)作为char
数组的终止符来实现字符串。终止空字符表示字符串序列的结尾。并非每个字符数组都是 C 字符串,但每个 C 字符串都是char
数组 。
由于字符串频繁出现在程序中,C 提供了一些库来操作字符串。使用 C 字符串库的程序需要包含string.h
. 大多数字符串库函数要求程序员为函数操作的字符数组分配空间。打印字符串的值时,请使用%s
占位符。
下面是一个使用字符串和一些字符串库函数的示例程序:
#include <stdio.h>
#include <string.h> // include the C string library
int main(void) {
char str1[10];
char str2[10];
str1[0] = 'h';
str1[1] = 'i';
str1[2] = '\0'; // explicitly add null terminating character to end
// strcpy copies the bytes from the source parameter (str1) to the
// destination parameter (str2) and null terminates the copy.
strcpy(str2, str1);
str2[1] = 'o';
printf("%s %s\n", str1, str2); // prints: hi ho
return 0;
}
2.6.2.动态分配字符串
字符数组可以动态分配(如 指针 和 数组 部分中所述)。当动态分配空间来存储字符串时,请务必记住在数组中为'\0'
字符串末尾的终止字符分配空间。
以下示例程序演示了静态和动态分配的字符串(注意传递给malloc
的值):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
int size;
char str[64]; // statically allocated
char *new_str = NULL; // for dynamically allocated
strcpy(str, "Hello");
size = strlen(str); // returns 5
new_str = malloc(sizeof(char) * (size+1)); // need space for '\0'
if(new_str == NULL) {
printf("Error: malloc failed! exiting.\n");
exit(1);
}
strcpy(new_str, str);
printf("%s %s\n", str, new_str); // prints "Hello Hello"
strcat(str, " There"); // concatenate " There" to the end of str
printf("%s\n", str); // prints "Hello There"
free(new_str); // free malloc'ed space when done
new_str = NULL;
return 0;
}
c 字符串函数和目标内存
许多 C 字符串函数(特别是strcpy
和 strcat
) 通过跟随 目标 字符串指针 (char *
) 参数并写入它指向的位置来存储其结果。此类函数假设目标包含足够的内存来存储结果。因此,作为程序员,您必须确保在调用这些函数之前目的地有足够的内存可用。
未能分配足够的内存将产生不确定的结果,包括程序崩溃和重大安全漏洞。例如,以下调用strcpy
和 strcat
演示新手 C 程序员经常犯的错误:
// Attempt to write a 12-byte string into a 5-character array.
char mystr[5];
strcpy(mystr, "hello world");
// Attempt to write to a string with a NULL destination.
char *mystr = NULL;
strcpy(mystr, "try again");
// Attempt to modify a read-only string literal.
char *mystr = "string literal value";
strcat(mystr, "string literals aren't writable");
2.6.3.用于操作 C 字符串和字符的库
C 提供了多个带有操作字符串和字符的函数的库。在编写使用 C 字符串的程序时,字符串库 (string.h
) 特别有用。stdlib.h
和 stdio.h
库还包含用于字符串操作的函数,并且ctype.h
库包含用于操作单个字符值的函数。
使用 C 字符串库函数时,请务必记住,大多数函数不会为其操作的字符串分配空间,也不会检查您传入的字符串是否有效;您的程序必须为 C 字符串库将使用的字符串分配空间。此外,如果库函数修改了传递的字符串,调用者需要确保该字符串的格式正确(即末尾有一个终止字符\0
)。使用错误的数组参数值调用字符串库函数通常会导致程序崩溃。不同库函数的文档(例如手册页)指定库函数是否分配空间,或者调用者是否负责将分配的空间传递给库函数。
`char[]`以及`char *`参数和`char *`返回类型
静态声明和动态分配的字符数组都可以传递给参数,char *
因为任一类型变量的名称都会计算为内存中数组的基地址。将参数声明为类型char []
也适用于静态和动态分配的参数值,但char *
更常用于指定字符串(数组char
)参数的类型。
如果函数返回一个字符串(其返回类型为 char *
),则其返回值只能赋给类型也是 的变量char *
;它不能分配给静态分配的数组变量。存在此限制是因为静态声明的数组变量的名称不是有效的左值 (其在内存中的基地址无法更改),因此无法为其分配char *
返回值。
strlen, strcpy, strncpy
字符串库提供了复制字符串和查找字符串长度的函数:
// returns the number of characters in the string (not including the null character)
int strlen(char *s);
// copies string src to string dst up until the first '\0' character in src
// (the caller needs to make sure src is initialized correctly and
// dst has enough space to store a copy of the src string)
// returns the address of the dst string
char *strcpy(char *dst, char *src);
// like strcpy but copies up to the first '\0' or size characters
// (this provides some safety to not copy beyond the bounds of the dst
// array if the src string is not well formed or is longer than the
// space available in the dst array); size_t is an unsigned integer type
char *strncpy(char *dst, char *src, size_t size);
当源字符串可能长于目标字符串的总容量时,使用strcpy
函数是不安全的。在这种情况下,应该使用strncpy
. 该size
参数在 strncpy
中停止将多于size
个数的字符从src
字符串复制到dst
字符串中。当 src
字符串的长度大于或等于 size
时,strncpy
将 src
字符串中前 size
个字符复制到 dst
字符串中,并且不在 dst
中添加添加终止符。因此,程序员应该在调用 strncpy
后显式添加一个终止字符到 dst
字符串的末尾。
以下是在程序中使用这些函数的一些示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // include the string library
int main(void) {
// variable declarations that will be used in examples
int len, i, ret;
char str[32];
char *d_str, *ptr;
strcpy(str, "Hello There");
len = strlen(str); // len is 11
d_str = malloc(sizeof(char) * (len+1));
if (d_str == NULL) {
printf("Error: malloc failed\n");
exit(1);
}
strncpy(d_str, str, 5);
d_str[5] = '\0'; // explicitly add null terminating character to end
printf("%d:%s\n", strlen(str), str); // prints 11:Hello There
printf("%d:%s\n", strlen(d_str), d_str); // prints 5:Hello
free(d_str);
return 0;
}
strlcpy(glibc2.38)
该strlcpy
函数与 类似strncpy
,但它总是将 '\0'
字符添加到目标字符串的末尾。始终终止字符串使其成为 strncpy 的更安全替代方案,因为它不需要程序员记住显式以 null 终止字符串。
// like strncpy but copies up to the first '\0' or size-1 characters
// and null terminates the dest string (if size > 0).
char *strlcpy(char *dest, char *src, size_t size);
Linux 的 GNU C 库添加strlcpy
到最新版本 (2.38)。目前它仅在某些系统上可用,但随着 C 库的新版本变得更加广泛,它的可用性将会增加。我们建议 strlcpy
在可用时使用。
在 strlcpy
可用的系统上,下面是调用 strncpy
的例子:
// 将最多 5 个字符从 str 复制到 d_str strncpy(d_str, str, 5);
d_str[5] = '\0'; // 显式添加空终止符到末尾
可以用以下调用替换strlcpy
:
// 将最多 5 个字符从 str 复制到 d_str
strlcpy(d_str, str, 6); // strlcpy 总是在末尾添加 '\0'
strcmp, strncmp
字符串库还提供了比较两个字符串的函数。使用==
运算符比较字符串变量_不会_比较字符串中的字符 - 它仅比较两个字符串的基地址。例如,表达式:
if (d_str == str) { ...
将 d_str
指向的堆中数组的基地址与 str
指向栈上分配的数组的基地址进行比较。
要比较字符串的值,程序员需要手动编写代码来比较相应的元素值,或者使用字符串库中的strcmp
或 strncmp
函数:
int strcmp(char *s1, char *s2);
// returns 0 if s1 and s2 are the same strings
// a value < 0 if s1 is less than s2
// a value > 0 if s1 is greater than s2
int strncmp(char *s1, char *s2, size_t n);
// compare s1 and s2 up to at most n characters
strcmp
函数根据字符串的 ASCII 表示形式逐个字符地比较字符串。换句话说,它比较两个参数数组相应位置的char
类型的值以产生字符串比较的结果,这有时会产生不直观的结果。例如,在 ASCII 编码中字符 'a'
的值大于字符 'Z'
的值。因此, strcmp("aaa", "Zoo")
返回一个正数表示 "aaa"
比 "Zoo"
大,strcmp("aaa", "zoo")
调用返回一个负数表示 "aaa"
比 "zoo"
小。
以下是一些字符串比较示例:
strcpy(str, "alligator");
strcpy(d_str, "Zebra");
ret = strcmp(str,d_str);
if (ret == 0) {
printf("%s is equal to %s\n", str, d_str);
} else if (ret < 0) {
printf("%s is less than %s\n", str, d_str);
} else {
printf("%s is greater than %s\n", str, d_str); // true for these strings
}
ret = strncmp(str, "all", 3); // returns 0: they are equal up to first 3 chars
strcat, strstr, strchr
字符串库函数可以连接字符串(请注意,调用者需要确保目标字符串有足够的空间来存储结果):
// append chars from src to end of dst
// returns ptr to dst and adds '\0' to end
char *strcat(char *dst, char *src)
// append the first chars from src to end of dst, up to a maximum of size
// returns ptr to dst and adds '\0' to end
char *strncat(char *dst, char *src, size_t size);
它还提供了在字符串中查找子字符串或字符值的函数:
// locate a substring inside a string
// (const means that the function doesn't modify string)
// returns a pointer to the beginning of substr in string
// returns NULL if substr not in string
char *strstr(const char *string, char *substr);
// locate a character (c) in the passed string (s)
// (const means that the function doesn't modify s)
// returns a pointer to the first occurrence of the char c in string
// or NULL if c is not in the string
char *strchr(const char *s, int c);
以下是使用这些函数的一些示例(为了可读性,我们省略了一些错误处理):
char str[32];
char *ptr;
strcpy(str, "Zebra fish");
strcat(str, " stripes"); // str gets "Zebra fish stripes"
printf("%s\n", str); // prints: Zebra fish stripes
strncat(str, " are black.", 8);
printf("%s\n", str); // prints: Zebra fish stripes are bla (spaces count)
ptr = strstr(str, "trip");
if (ptr != NULL) {
printf("%s\n", ptr); // prints: tripes are bla
}
ptr = strchr(str, 'e');
if (ptr != NULL) {
printf("%s\n", ptr); // prints: ebra fish stripes are bla
}
分别调用 strchr
和 strstr
返回参数数组中具有匹配字符或匹配子字符串的第一个元素的地址。该元素地址是在以 \0
结尾的 char
字符数组的开始处。换句话说,ptr
指向另一个字符串内的子字符串的开头。当使用 printf
把 ptr
的值作为字符串打印时,会把 ptr
指针指向的字符数组开始之后的下标对应的字符都打印出来, 从而产生上面列出的结果。
strtok, strtok_r
字符串库还提供将字符串划分为token的函数。token(分词)是指字符串中由程序员选择的任意数量的分隔符分隔的字符子序列。
char *strtok(char *str, const char *delim);
// a reentrant version of strtok (reentrant is defined in later chapters):
char *strtok_r(char *str, const char *delim, char **saveptr);
strtok
(或 strtok_r
)函数在较大的字符串中查找单个标记。例如,将 strtok
的分隔符设置为空白字符集会在最初包含英语句子的字符串中生成单词。也就是说,句子中的每个单词都是字符串中的一个标记。
下面是一个示例程序,用于strtok
查找单个单词作为输入字符串中的标记。(也可以从此处复制: strtokexample.c)。
/*
* Extract whitespace-delimited tokens from a line of input
* and print them one per line.
*
* to compile:
* gcc -g -Wall strtokexample.c
*
* example run:
* Enter a line of text: aaaaa bbbbbbbbb cccccc
*
* The input line is:
* aaaaa bbbbbbbbb cccccc
* Next token is aaaaa
* Next token is bbbbbbbbb
* Next token is cccccc
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void) {
/* whitespace stores the delim string passed to strtok. The delim
* string is initialized to the set of characters that delimit tokens
* We initialize the delim string to the following set of chars:
* ' ': space '\t': tab '\f': form feed '\r': carriage return
* '\v': vertical tab '\n': new line
* (run "man ascii" to list all ASCII characters)
*
* This line shows one way to statically initialize a string variable
* (using this method the string contents are constant, meaning that they
* cannot be modified, which is fine for the way we are using the
* whitespace string in this program).
*/
char *whitespace = " \t\f\r\v\n"; /* Note the space char at beginning */
char *token; /* The next token in the line. */
char *line; /* The line of text read in that we will tokenize. */
/* Allocate some space for the user's string on the heap. */
line = malloc(200 * sizeof(char));
if (line == NULL) {
printf("Error: malloc failed\n");
exit(1);
}
/* Read in a line entered by the user from "standard in". */
printf("Enter a line of text:\n");
line = fgets(line, 200 * sizeof(char), stdin);
if (line == NULL) {
printf("Error: reading input failed, exiting...\n");
exit(1);
}
printf("The input line is:\n%s\n", line);
/* Divide the string into tokens. */
token = strtok(line, whitespace); /* get the first token */
while (token != NULL) {
printf("Next token is %s\n", token);
token = strtok(NULL, whitespace); /* get the next token */
}
free(line);
return 0;
}
sprintf
C语言 stdio
库还提供了操作 C 字符串的函数。也许最有用的是该sprintf
函数,它“打印”到字符串中,而不是将输出打印到终端:
// like printf(), the format string allows for placeholders like %d, %f, etc.
// pass parameters after the format string to fill them in
int sprintf(char *s, const char *format, ...);
sprintf
从各种类型的值初始化字符串的内容。它的参数format
类似于printf
和 的参数scanf
。这里有些例子:
char str[64];
float ave = 76.8;
int num = 2;
// initialize str to format string, filling in each placeholder with
// a char representation of its arguments' values
sprintf(str, "%s is %d years old and in grade %d", "Henry", 12, 7);
printf("%s\n", str); // prints: Henry is 12 years old and in grade 7
sprintf(str, "The average grade on exam %d is %g", num, ave);
printf("%s\n", str); // prints: The average grade on exam 2 is 76.8
单个字符值的函数
标准 C 库(stdlib.h
) 包含一组用于操作和测试各个 char
值的函数,包括:
#include <stdlib.h> // include stdlib and ctypes to use these
#include <ctype.h>
int islower(ch);
int isupper(ch); // these functions return a non-zero value if the
int isalpha(ch); // test is TRUE, otherwise they return 0 (FALSE)
int isdigit(ch);
int isalnum(ch);
int ispunct(ch);
int isspace(ch);
char tolower(ch); // returns ASCII value of lower-case of argument
char toupper(ch);
以下是它们的一些使用示例:
char str[64];
int len, i;
strcpy(str, "I see 20 ZEBRAS, GOATS, and COWS");
if ( islower(str[2]) ){
printf("%c is lower case\n", str[2]); // prints: s is lower case
}
len = strlen(str);
for (i = 0; i < len; i++) {
if ( isupper(str[i]) ) {
str[i] = tolower(str[i]);
} else if( isdigit(str[i]) ) {
str[i] = 'X';
}
}
printf("%s\n", str); // prints: i see XX zebras, goats, and cows
将字符串转换为其他类型的函数
stdlib.h
还包含在字符串和其他 C 类型之间进行转换的函数。例如:
#include <stdlib.h>
int atoi(const char *nptr); // convert a string to an integer
double atof(const char *nptr); // convert a string to a float
这是一个例子:
printf("%d %g\n", atoi("1234"), atof("4.56"));
有关这些和其他 C 库函数的更多信息(包括它们的用途、参数格式、返回内容以及需要包含哪些标头才能使用它们),请参阅它们的 手册页。例如,要查看strcpy
手册页,请运行:
$ man strcpy